/*
 * Copyright (C) 2010 M. Hautle.
 * 
 * This file is part of TrackedBean.
 * 
 * TrackedBean is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any later version.
 * 
 * TrackedBean is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along with TrackedBean. If not, see <http://www.gnu.org/licenses/>.
 */
package ch.trackedbean.internal;

import static ch.msoftch.asm.Opcodes.*;
import java.lang.reflect.*;
import java.util.*;
import ch.msoftch.asm.*;
import ch.msoftch.asm.Type;
import ch.msoftch.asm.tree.*;
import ch.msoftch.internal.*;

/**
 * Helper class creating a wrapper implementation of a interface.<br>
 * Use this helper if you need to a simple kind of proxy, just forwarding the calls to a real, but exchangable, implementation.
 * 
 * @author M. Hautle
 * @since 17.10.2010
 */
public final class ServiceWrapper {
    /** The name of the instance field. */
    private static final String DELEGATE_FIELD = "_impl";

    /** The internal name of {@link IWrapper}. */
    private static final String WRAPPER_TYPE = Type.getInternalName(IWrapper.class);

    /** Internal name of {@link Object}. */
    private static final String OBJECT = Type.getInternalName(Object.class);

    /**
     * Hidden constructor.
     */
    private ServiceWrapper() {
    }

    /**
     * Creates a wrapper object for the given interface.<br>
     * Use {@link #setImplementation(Object, Object)} to set the implementation to be used by the wrapper.
     * 
     * @param <T> The interface type
     * @param c The interface type
     * @param defaultType The default/fallback implementation to use (must have a default constructor)
     * @return The wrapper instance
     */
    public static <T> T createWrapper(Class<T> c, Class<? extends T> defaultType) {
        try {
            return getWrapperClass(c, defaultType).newInstance();
        } catch (IllegalArgumentException e) {
            throw e;
        } catch (Exception e) {
            throw new TechnicalException("Error while creating a wrapper for " + c.getName(), e);
        }
    }

    /**
     * Sets the implementation of a wrapper.<br>
     * This method works only for wrappers created by {@link #createWrapper(Class, Class)}.
     * 
     * @param <T> The wrapper type
     * @param <I> The implementation type
     * @param w The wrapper
     * @param impl The implementation or null to use the default/fallback
     * @return The wrapper
     */
    public static <T, I extends T> T setImplementation(T w, I impl) {
        if (w instanceof IWrapper)
            ((IWrapper) w)._setImplementation(impl);
        return w;
    }

    /**
     * Returns a wrapper class for the given type.
     * 
     * @param <T> The interface type
     * @param c The interface type
     * @param defaultType The default/fallback implementation to use (must have a default constructor)
     * @return The wrapper class
     * @throws IllegalArgumentException If an invalid type gets passed
     */
    @SuppressWarnings("unchecked")
    private static synchronized <T> Class<? extends T> getWrapperClass(Class<T> c, Class<? extends T> defaultType) throws IllegalArgumentException {
        if (c == null || !c.isInterface())
            throw new IllegalArgumentException("Only interfaces were accepted!");
        final String name = buildName(defaultType);
        final ASMClassLoader loader = ASMClassLoader.getLoader(c.getClassLoader());
        Class cl = loader.getLoadedClass(name);
        if (cl == null)
            cl = loader.defineClass(name, build(name, c, defaultType));
        return cl;
    }

    /**
     * Builds the name for the recorder of the given class.
     * 
     * @param c The class
     * @return The name for the recorder
     */
    private static String buildName(Class<?> c) {
        final String name = c.getName();
        if (name.startsWith("java."))
            return "my" + name + "$wrapper";
        return name.concat("$wrapper");
    }

    /**
     * Builds the recorder class.
     * 
     * @param <T> The interface type
     * @param name The name of the new class
     * @param c The interface class
     * @param defaultType The default implementation
     * @return The bytecode
     */
    @SuppressWarnings("unchecked")
    private static <T> byte[] build(String name, Class<T> c, Class<? extends T> defaultType) {
        final String interfaze = Type.getInternalName(c);
        final String fieldDesc = "L" + interfaze + ";";
        final ClassNode cn = new ClassNode();
        cn.name = name.replace('.', '/');
        cn.access = ACC_PUBLIC | ACC_FINAL;
        cn.version = V1_5;
        cn.superName = OBJECT;
        cn.interfaces.add(interfaze);
        cn.interfaces.add(WRAPPER_TYPE);
        // the field for the delegate
        cn.fields.add(new FieldNode(ACC_PRIVATE, DELEGATE_FIELD, fieldDesc, null, null));
        createDefaultConstructor(cn);
        final List methods = cn.methods;
        methods.add(createDelegateSetter(cn.name, interfaze, fieldDesc, Type.getInternalName(defaultType)));
        // implement all other delegate methods
        for (Method m : c.getMethods())
            methods.add(createDelegate(m, cn.name, interfaze, fieldDesc));
        final ClassWriter w = new ClassWriter(ClassWriter.COMPUTE_MAXS);
        cn.accept(w);
        return w.toByteArray();
    }

    /**
     * Adds a default constructor.
     * 
     * @param cn The classnode
     */
    @SuppressWarnings("unchecked")
    private static void createDefaultConstructor(ClassNode cn) {
        final MethodNode c = new MethodNode(ACC_PUBLIC, "<init>", "()V", null, null);
        final InsnList inst = c.instructions;
        inst.add(new VarInsnNode(ALOAD, 0));
        inst.add(new MethodInsnNode(INVOKESPECIAL, cn.superName, "<init>", "()V"));
        inst.add(new InsnNode(RETURN));
        c.maxLocals = 1;
        c.maxStack = 1;
        cn.methods.add(c);
    }

    /**
     * Implements the {@link IWrapper#_setImplementation(Object)} method.
     * 
     * @param type The internal name of the class being built
     * @param interfaze The internal name of the wrapped type
     * @param fieldDesc The field description of the {@link #DELEGATE_FIELD}
     * @param defaultType The internal name of the default implementation
     * @return The method
     */
    private static MethodNode createDelegateSetter(String type, String interfaze, String fieldDesc, String defaultType) {
        final MethodNode m = new MethodNode(ACC_PUBLIC, "_setImplementation", "(Ljava/lang/Object;)V", null, null);
        final InsnList insn = m.instructions;
        insn.add(new VarInsnNode(ALOAD, 1));
        final LabelNode l = new LabelNode();
        insn.add(new JumpInsnNode(IFNONNULL, l));
        // value was null - create and set the default implementation
        insn.add(new VarInsnNode(ALOAD, 0));
        insn.add(new TypeInsnNode(NEW, defaultType));
        insn.add(new InsnNode(DUP));
        insn.add(new MethodInsnNode(INVOKESPECIAL, defaultType, "<init>", "()V"));
        insn.add(new FieldInsnNode(PUTFIELD, type, DELEGATE_FIELD, fieldDesc));
        insn.add(new InsnNode(RETURN));
        // value was not null - just cast and set it
        insn.add(l);
        insn.add(new VarInsnNode(ALOAD, 0));
        insn.add(new VarInsnNode(ALOAD, 1));
        insn.add(new TypeInsnNode(CHECKCAST, interfaze));
        insn.add(new FieldInsnNode(PUTFIELD, type, DELEGATE_FIELD, fieldDesc));
        insn.add(new InsnNode(RETURN));
        return m;
    }

    /**
     * Creates a delegate method for the given method.
     * 
     * @param m The method
     * @param type The internal name of the class being built
     * @param interfaze The internal name of the interface to implement
     * @param fieldDesc The field description of the {@link #DELEGATE_FIELD}
     * @return The method
     */
    private static MethodNode createDelegate(Method m, String type, String interfaze, String fieldDesc) {
        final String desc = Type.getMethodDescriptor(m);
        final MethodNode mn = new MethodNode(ACC_PUBLIC | ACC_FINAL, m.getName(), desc, null, getInternalNames(m.getExceptionTypes()));
        final InsnList inst = mn.instructions;
        inst.add(new VarInsnNode(ALOAD, 0));
        inst.add(new FieldInsnNode(GETFIELD, type, DELEGATE_FIELD, fieldDesc));
        int load = 1;
        for (Class c : m.getParameterTypes()) {
            final Type t = Type.getType(c);
            inst.add(new VarInsnNode(t.getOpcode(ILOAD), load));
            load += t.getSize();
        }
        inst.add(new MethodInsnNode(INVOKEINTERFACE, interfaze, m.getName(), desc));
        inst.add(new InsnNode(Type.getReturnType(m).getOpcode(IRETURN)));
        return mn;
    }

    /**
     * Returns the internal names of the passed types.
     * 
     * @param types The types to convert
     * @return The internal names or null if the passed array was empty
     */
    private static String[] getInternalNames(Class<?>[] types) {
        if (types.length == 0)
            return null;
        final String[] res = new String[types.length];
        for (int i = 0; i < res.length; i++)
            res[i] = Type.getInternalName(types[i]);
        return res;
    }

    /**
     * Interface implemented by all wrapper classes to allow the injection of the real instance.
     * 
     * @author M. Hautle
     * @since 17.10.2010
     */
    public interface IWrapper {
        /**
         * Sets the real instance.
         * 
         * @param o The real instance or null for the default instance
         */
        void _setImplementation(Object o);
    }
}
